﻿#region Usings
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Kaleta.Xna.Distributed;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Kaleta.Xna.Collision;
using Kaleta.Network;
using Kaleta.Xna.Extensions;
using Kaleta.Xna.Timing;
using Kaleta.Xna.Debug;
using Kaleta.Outcast.Network;
#endregion

namespace Kaleta.Outcast
{
    public class Ship : DistributedSolidBody
    {
        #region Constructor
        public Ship()
        {
            this.Texture = Global.SpriteSheets.Ship.Texture;
            this.Body = new Body()
            {
                Origin = new Vector2(this.Texture.Width / 2f, this.Texture.Height / 2f),
                Scale = new Vector2(0.5f)
            };

            this.Position = new Vector2(100, 100);
            this.MovementDampingEnabled = true;
            this.MaxVelocity = 2.5f;

            syncAction = new BoolRecharger(500f);

            #region Registration of distributed properties
            _movementState = DistributableProperty<MovementState>.Register("MovementState", this, Lidgren.Network.NetChannel.ReliableInOrder1);
            _syncData = DistributableProperty<ShipSyncData>.Register("SyncData", this, Lidgren.Network.NetChannel.ReliableInOrder1);
            #endregion

        }
        #endregion

        public override void Draw(SpriteBatch batch)
        {
            batch.Draw(this.Texture, this.Position, null, Color.White, this.Body.Direction, this.Body.Origin, this.Body.Scale, SpriteEffects.None, 0);
        }

        public override void Update(GameTime gameTime)
        {
            //only host will sync:
            //if (DistributionSession.Server != null)
            //    if (this.syncAction.TryDrain())
            //        SynchronizeWithServer();


            UpdateMovement();
            UpdateMovementGhost();

            ApplySmoothing(gameTime);

            Debugger.SetValue("Sync Diff Pos.X-ghost.X", this.Position.X - this.ghostPosition.X);
            Debugger.SetValue("Velocity", this.Velocity.Length());
            Debugger.SetValue("Acceleration", this.Acceleration);


            this.syncAction.Update(gameTime);
            this.storyboard.Update(gameTime);
        }

        #region Private methods

        private void ApplySmoothing(GameTime gameTime)
        {
            float timeToSync = 500f;

            if (this.lastSyncRecieved == -1 || lastSyncRecieved == timeToSync) return;

            this.Rotation = MathHelper.Lerp(this.Rotation, this.ghostRotation, lastSyncRecieved / timeToSync);
            this.Acceleration = MathHelper.Lerp(this.Acceleration, this.ghostAcceleration, lastSyncRecieved / timeToSync);
            this.Direction = MathHelper.Lerp(this.Direction, this.ghostDirection, lastSyncRecieved / timeToSync);

            this.Velocity = new Vector2()
            {
                X = MathHelper.Lerp(this.Velocity.X, this.ghostVelocity.X, lastSyncRecieved / timeToSync),
                Y = MathHelper.Lerp(this.Velocity.Y, this.ghostVelocity.Y, lastSyncRecieved / timeToSync)
            };
            this.Position = new Vector2()
            {
                X = MathHelper.Lerp(this.Position.X, this.ghostPosition.X, lastSyncRecieved / timeToSync),
                Y = MathHelper.Lerp(this.Position.Y, this.ghostPosition.Y, lastSyncRecieved / timeToSync)
            };

            
            lastSyncRecieved += (float) gameTime.ElapsedGameTime.TotalMilliseconds;
            if (lastSyncRecieved > timeToSync) lastSyncRecieved = timeToSync;

        }

        private float lastSyncRecieved = -1f;

        private void UpdateMovementGhost()
        {
            if (this.MovementState.HasFlag(MovementState.Breaking))
            {
                this.ghostAcceleration = 0f;
                this.ghostVelocity = new Vector2()
                {
                    X = this.ghostVelocity.X - 0.05f * this.ghostVelocity.X,
                    Y = this.ghostVelocity.Y - 0.05f * this.ghostVelocity.Y
                };
            }
            else if (this.MovementState.HasFlag(MovementState.Accelerating))
            {
                if (this.ghostAcceleration < 0.1f)
                    this.ghostAcceleration = 0.1f;
            }
            else
                this.ghostAcceleration = 0f;

            if (this.MovementState.HasFlag(MovementState.RotatingRight))
                this.ghostRotation = 0.04f;
            else if (this.MovementState.HasFlag(MovementState.RotatingLeft))
                this.ghostRotation = -0.04f;
            else
                this.ghostRotation = 0f;

            Vector2 value = this.ghostDirection.GetNormalizedVector2() * this.ghostAcceleration + this.ghostVelocity;
            if (this.MovementDampingEnabled) this.ghostVelocity = value.Clamp(this.MaxVelocity); else this.ghostVelocity = value;

            this.ghostPosition = new Vector2()
            {
                X = this.ghostPosition.X + this.ghostVelocity.X,
                Y = this.ghostPosition.Y + this.ghostVelocity.Y
            };

            this.ghostDirection += this.ghostRotation;

        }

        private void UpdateMovement()
        {
            if (this.MovementState.HasFlag(MovementState.Breaking))
            {
                this.Acceleration = 0f;
                this.Velocity = new Vector2()
                {
                    X = this.Velocity.X - 0.05f * this.Velocity.X,
                    Y = this.Velocity.Y - 0.05f * this.Velocity.Y
                };
            }
            else if (this.MovementState.HasFlag(MovementState.Accelerating))
            {
                if (this.Acceleration < 0.1f)
                    this.Acceleration = 0.1f;
            }
            else
                this.Acceleration = 0f;

            if (this.MovementState.HasFlag(MovementState.RotatingRight))
                this.Rotation = 0.04f;
            else if (this.MovementState.HasFlag(MovementState.RotatingLeft))
                this.Rotation = -0.04f;
            else
                this.Rotation = 0f;

            this.Velocity = this.Direction.GetNormalizedVector2() * this.Acceleration + this.Velocity;

            this.Body.Position = new Vector2()
            {
                X = this.Body.Position.X + this.Velocity.X,
                Y = this.Body.Position.Y + this.Velocity.Y
            };

            this.Body.Direction += this.Rotation;

        }

        /// <summary>
        /// Should used only on Host side. Sends sync data to server to synchronize ship with all other clients.
        /// </summary>
        public void SynchronizeWithServer()
        {
            if (!this.syncAction.TryDrain()) return;

            //save movement-state variables:
            Vector2 tempPosition = this.Position;
            Vector2 tempVelocity = this.Velocity;
            float tempRotation = this.Rotation;
            float tempAccel = this.Acceleration;
            float tempDirection = this.Direction;

            //lets calculate 'prediction' values of my sync data - update them according to my latency
            for (int i = 0; i < (int)(GetLatency() / Global.FrameUpdateTime); i++)
            {
                //update position times:
                this.UpdateMovement();
            }

            //send the 'prediction' (according to latency) updated sync data:
            SyncData = new ShipSyncData() { Position = this.Position, Rotation = this.Rotation, Direction = this.Direction, Acceleration = this.Acceleration, Velocity = this.Velocity };

            //set back movement-state variables to previous state:
            this.Position = tempPosition;
            this.Velocity = tempVelocity;
            this.Rotation = tempRotation;
            this.Acceleration = tempAccel;
            this.Direction = tempDirection;

        }
        #endregion

        #region User Commands

        public void Fire()
        {
            Bullet bullet = new Bullet();
            bullet.Position = this.Position;
            bullet.Direction = this.Direction;
            bullet.Velocity = this.Direction.GetNormalizedVector2() * 3f;

            DistributionSession.DistributeObject(bullet);

            bullet.Livetime = Livetime.Created;

        }

        public void Accelerate()
        {
            if (!this.MovementState.HasFlag(MovementState.Accelerating))
                this.MovementState |= MovementState.Accelerating;
        }

        public void Break()
        {
            if (!this.MovementState.HasFlag(MovementState.Breaking))
                this.MovementState |= MovementState.Breaking;
        }

        public void RotateLeft()
        {
            if (!this.MovementState.HasFlag(MovementState.RotatingLeft))
                this.MovementState |= MovementState.RotatingLeft;
        }

        public void RotateRight()
        {
            if (!this.MovementState.HasFlag(MovementState.RotatingRight))
                this.MovementState |= MovementState.RotatingRight;
        }

        #endregion

        #region Server-side events
        protected void OnMovementStateUpdate(ref MovementState oldValue)
        {
            var update = this.MovementState & ~oldValue;

            if (update.HasFlag(MovementState.Accelerating))
                this.MovementState &= ~MovementState.Breaking;

            if (update.HasFlag(MovementState.Breaking))
                this.MovementState &= ~MovementState.Accelerating;

            if (update.HasFlag(MovementState.RotatingLeft))
                this.MovementState &= ~MovementState.RotatingRight;

            if (update.HasFlag(MovementState.RotatingRight))
                this.MovementState &= ~MovementState.RotatingLeft;
        }

        public override void OnCollision(IBody obstacle, List<BodyShape> intersectingColliderShapes, List<BodyShape> intersectingObstacleShapes)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Invokes if sync data from server(host) has been recieved. Updates ships position according to sync data (Prediction algorithm).
        /// </summary>
        private void OnSynchronize()
        {
            lastSyncRecieved = 0f;

            //save movement-state variables:
            Vector2 tempPosition = this.Position;
            Vector2 tempVelocity = this.Velocity;
            float tempRotation = this.Rotation;
            float tempAccel = this.Acceleration;
            float tempDirection = this.Direction;

            //from server:
            this.Position = SyncData.Position;
            this.Velocity = SyncData.Velocity;
            this.Rotation = SyncData.Rotation;
            this.Direction = SyncData.Direction;
            this.Acceleration = SyncData.Acceleration;

            //then update movement variables according to my latency (only half of it, not the whole roundtrip latency):

            //prediction algorithm:
            for (int i = 0; i < (int)(GetLatency() / Global.FrameUpdateTime); i++)
                this.UpdateMovement();

            this.ghostAcceleration = this.Acceleration;
            this.ghostDirection = this.Direction;
            this.ghostPosition = this.Position;
            this.ghostRotation = this.Rotation;
            this.ghostVelocity = this.Velocity;

            //set back movement-state variables to previous state:
            this.Position = tempPosition;
            this.Velocity = tempVelocity;
            this.Rotation = tempRotation;
            this.Acceleration = tempAccel;
            this.Direction = tempDirection;

            Debugger.SetValue("Sync", this.Position.ToString());
            

        }

        private float GetLatency()
        {
            return DistributionSession.LatencyTester.GetLatency() / 2f;
        }

        #endregion

        #region Properties

        #region Public Properties

        public float Acceleration { get; protected set; }
        public float MaxVelocity { get; protected set; }
        public float Rotation { get; protected set; }



        private Vector2 _velocity;
        public Vector2 Velocity
        {
            get { return _velocity; }
            protected set { if (this.MovementDampingEnabled) _velocity = value.Clamp(this.MaxVelocity); else _velocity = value; }
        }


        #endregion

        #region Distributed Properties

        [Distribute]
        public MovementState MovementState
        {
            get
            {
                return _movementState.GetValue();
            }
            set
            {
                MovementState oldValue;

                if (_movementState.SetValue(value, out oldValue))
                    OnMovementStateUpdate(ref oldValue);
            }
        }
        private DistributableProperty<MovementState> _movementState;


        [Distribute]
        public ShipSyncData SyncData
        {
            get
            {
                return _syncData.GetValue();
            }
            set
            {
                if (_syncData.SetValue(value))
                    OnSynchronize();
            }
        }
        private DistributableProperty<ShipSyncData> _syncData;

       

        

        #endregion

        #region Private Properties
        private bool MovementDampingEnabled { get; set; }
        private Storyboard storyboard = new Storyboard();
        private BoolRecharger syncAction;

        public Vector2 ghostPosition;
        private Vector2 ghostVelocity;
        private float ghostRotation;
        private float ghostAcceleration;
        private float ghostDirection;


        #endregion

        #endregion

    }

    #region Enums

    [Flags]
    public enum MovementState : uint
    {
        Accelerating = 1,
        Breaking = 2,
        RotatingLeft = 4,
        RotatingRight = 8,
        BoostRotatingLeft = 16,
        BoostRotatingRight = 32,
        BoostAccelerating = 64,
        BoostReversingDirection = 128,
    }

    public static class EnumExtensions
    {
        public static bool HasFlag(this MovementState item, MovementState query)
        {
            return ((item & query) == query);
        }
    }

    #endregion

}
